// Copyright 2015 rain1017.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License. See the AUTHORS file
// for names of contributors.

/* jshint ignore:start */

/*!
 * Module dependencies.
 */

var MongooseCollection = require('mongoose/lib/collection')
    , Collection = require('mongodb').Collection
    , STATES = require('mongoose/lib/connectionstate')
    , utils = require('mongoose/lib/utils')
    , uuid = require('node-uuid')
    , logger = require('memdb-logger').getLogger('memdb-client', __filename);

/**
 * A [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) collection implementation.
 *
 * All methods methods from the [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) driver are copied and wrapped in queue management.
 *
 * @inherits Collection
 * @api private
 */

function MemdbCollection () {
    this.memdb = true;

    this.collection = null;
    MongooseCollection.apply(this, arguments);
    this.id = uuid.v4();

    var self = this;
    Object.defineProperty(this, '_memdbCollection', {
        get : function(){
            return require('../mdbgoose').autoconn.collection(self.name);
        },
    });
}

/*!
 * Inherit from abstract Collection.
 */

MemdbCollection.prototype.__proto__ = MongooseCollection.prototype;

/**
 * Called when the connection opens.
 *
 * @api private
 */

MemdbCollection.prototype.onOpen = function () {
    var self = this;

    // always get a new collection in case the user changed host:port
    // of parent db instance when re-opening the connection.

    if (!self.opts.capped.size) {
        // non-capped
        return self.conn.db.collection(self.name, callback);
    }

    // capped
    return self.conn.db.collection(self.name, function (err, c) {
        if (err) return callback(err);

        // discover if this collection exists and if it is capped
        self.conn.db.collection( 'system.namespaces', function(err, namespaces) {
            var namespaceName = self.conn.db.databaseName + '.' + self.name;
            namespaces.findOne({ name : namespaceName }, function(err, doc) {
                if (err) {
                    return callback(err);
                }
                var exists = !!doc;

                if (exists) {
                    if (doc.options && doc.options.capped) {
                        callback(null, c);
                    } else {
                        var msg = 'A non-capped collection exists with the name: '+ self.name +'\n\n'
                                        + ' To use this collection as a capped collection, please '
                                        + 'first convert it.\n'
                                        + ' http://www.mongodb.org/display/DOCS/Capped+Collections#CappedCollections-Convertingacollectiontocapped'
                        err = new Error(msg);
                        callback(err);
                    }
                } else {
                    // create
                    var opts = utils.clone(self.opts.capped);
                    opts.capped = true;
                    self.conn.db.createCollection(self.name, opts, callback);
                }
            });
        });
    });

    function callback (err, collection) {
        if (err) {
            // likely a strict mode error
            self.conn.emit('error', err);
        } else {
            self.collection = collection;
            MongooseCollection.prototype.onOpen.call(self);
        }
    };
};

/**
 * Called when the connection closes
 *
 * @api private
 */

MemdbCollection.prototype.onClose = function () {
    MongooseCollection.prototype.onClose.call(this);
};

/*!
 * Copy the collection methods and make them subject to queues
 */

for (var i in Collection.prototype) {
    // Ignore methods with Async suffix (Generated by bluebird)
    if(/Async$/.test(i)){
        continue;
    }

    (function(i){
        var needRename = ['find', 'findOne', 'count']//, 'distinct', 'aggregate', 'group', 'mapReduce', 'geoNear', 'geoHaystackSearch'];
        var funcName = i;
        if (needRename.indexOf(i) != -1) {
            funcName = i + 'Mongo';
        }

        MemdbCollection.prototype[funcName] = function () {
            if (this.buffer) {
                this.addQueue(i, arguments);
                return;
            }

            var collection = this.collection
                , args = arguments
                , self = this
                , debug = self.conn.base.options.debug;

            if (debug) {
                if ('function' === typeof debug) {
                    debug.apply(debug
                        , [self.name, i].concat(utils.args(args, 0, args.length-1)));
                } else {
                    console.error('\x1B[0;36mMongoose:\x1B[0m %s.%s(%s) %s %s %s'
                        , self.name
                        , i
                        , print(args[0])
                        , print(args[1])
                        , print(args[2])
                        , print(args[3]))
                }
            }

            return collection[i].apply(collection, args);
        };
    })(i);
}


MemdbCollection.prototype.find = function(query, options, cb){
    logger.debug('MemdbCollection.find %j %j', query, options);

    if(options.comment === '$mongo'){
        delete options.comment;
        return this.findMongo(query, options, cb);
    }
    else if(options.comment === '$readonly'){
        delete options.comment;
        options.readonly = true;
    }

    return this._memdbCollection.find(query, options.fields, options)
    .then(function(docs){
        return {
            toArray : function(cb){
                cb(null, docs);
            }
        };
    })
    .nodeify(cb);
};

MemdbCollection.prototype.findOne = function(query, options, cb){
    logger.debug('MemdbCollection.findOne %j %j', query, options);

    if(options.comment === '$mongo'){
        delete options.comment;
        return this.findOneMongo(query, options, cb);
    }
    else if(options.comment === '$readonly'){
        delete options.comment;
        options.readonly = true;
    }

    return this._memdbCollection.findOne(query, options.fields, options)
    .nodeify(cb);
};

MemdbCollection.prototype.insert = function(docs, opts, cb) {
    logger.debug('MemdbCollection.insert');

    return this._memdbCollection.insert(docs)
    .nodeify(cb);
};

MemdbCollection.prototype.insertOne = MemdbCollection.prototype.insert;
MemdbCollection.prototype.insertMany = MemdbCollection.prototype.insert;

MemdbCollection.prototype.remove = function(selector, opts, cb) {
    logger.debug('MemdbCollection.remove %j', selector);
    return this._memdbCollection.remove(selector, opts)
    .nodeify(cb);
};

MemdbCollection.prototype.removeOne = function(selector, opts, cb) {
    logger.debug('MemdbCollection.removeOne %j', selector);
    opts = opts || {};
    opts.limit = 1;
    return this._memdbCollection.remove(selector, opts)
    .nodeify(cb);
};

MemdbCollection.prototype.deleteOne = MemdbCollection.prototype.removeOne;
MemdbCollection.prototype.deleteMany = MemdbCollection.prototype.remove;

MemdbCollection.prototype.update = function(selector, doc, opts, cb) {
    logger.debug('MemdbCollection.update %j', selector);
    return this._memdbCollection.update(selector, doc, opts)
    .nodeify(cb);
};

MemdbCollection.prototype.updateOne = function(selector, doc, opts, cb) {
    logger.debug('MemdbCollection.updateOne %j', selector);
    opts = opts || {};
    opts.limit = 1;
    return this._memdbCollection.update(selector, doc, opts)
    .nodeify(cb);
};

MemdbCollection.prototype.updateMany = MemdbCollection.prototype.update;
MemdbCollection.prototype.replaceOne = MemdbCollection.prototype.updateOne;

MemdbCollection.prototype.count = function(query, opts, cb) {
    logger.debug('MemdbCollection.count %j', query);
    return this._memdbCollection.count(query, opts)
    .nodeify(cb);
};

var unimplemented = ['bulkWrite', 'save', 'findAndModify', 'findAndRemove', 'findOneAndDelete', 'findOneAndReplace', 'findOneAndUpdate', 'rename', 'drop'];
unimplemented.forEach(function(method){
    MemdbCollection.prototype[method] = function(){
        throw new Error('Collection#' + method + ' unimplemented by driver');
    }
});

/*!
 * Debug print helper
 */

function print (arg) {
    var type = typeof arg;
    if ('function' === type || 'undefined' === type) return '';
    return format(arg);
}

/*!
 * Debug print helper
 */

function format (obj, sub) {
    var x = utils.clone(obj);
    if (x) {
        if ('Binary' === x.constructor.name) {
            x = '[object Buffer]';
        } else if ('ObjectID' === x.constructor.name) {
            var representation = 'ObjectId("' + x.toHexString() + '")';
            x = { inspect: function() { return representation; } };
        } else if ('Date' === x.constructor.name) {
            var representation = 'new Date("' + x.toUTCString() + '")';
            x = { inspect: function() { return representation; } };
        } else if ('Object' === x.constructor.name) {
            var keys = Object.keys(x)
                , i = keys.length
                , key
            while (i--) {
                key = keys[i];
                if (x[key]) {
                    if ('Binary' === x[key].constructor.name) {
                        x[key] = '[object Buffer]';
                    } else if ('Object' === x[key].constructor.name) {
                        x[key] = format(x[key], true);
                    } else if ('ObjectID' === x[key].constructor.name) {
                        ;(function(x){
                            var representation = 'ObjectId("' + x[key].toHexString() + '")';
                            x[key] = { inspect: function() { return representation; } };
                        })(x)
                    } else if ('Date' === x[key].constructor.name) {
                        ;(function(x){
                            var representation = 'new Date("' + x[key].toUTCString() + '")';
                            x[key] = { inspect: function() { return representation; } };
                        })(x)
                    } else if (Array.isArray(x[key])) {
                        x[key] = x[key].map(function (o) {
                            return format(o, true)
                        });
                    }
                }
            }
        }
        if (sub) return x;
    }

    return require('util')
        .inspect(x, false, 10, true)
        .replace(/\n/g, '')
        .replace(/\s{2,}/g, ' ')
}

/**
 * Retreives information about this collections indexes.
 *
 * @param {Function} callback
 * @method getIndexes
 * @api public
 */

MemdbCollection.prototype.getIndexes = MemdbCollection.prototype.indexInformation;

/*!
 * Module exports.
 */

module.exports = MemdbCollection;
